Aprende en 10 minutos como funciona el Recolector de Basura de JS

Todo proceso en computación necesita de cierto espacio en memoria para ejecutarse.

Cuando tu ejecutas un programa, cada variable que declaras, cada función, cada dato que manejas, sea un objeto o sea un valor primitivo, todo ocupa memoria.

Pero creo que es obvio decir que no todo lo que se se guarda en memoria se queda para siempre en tu computadora. Si ese fuera el caso, la capacidad de nuestros ordenadores se acabaría en un abrir y cerrar de ojos. ¿Cómo es que Javascript sabe cuales cosas pueden dejar de ocupar memoria y cuáles no? ¿Y como libera la memoria una vez decide hacerlo?

Bueno, primero lo primero. Para decidir si debe liberar memoria o no, Javascript utiliza un concepto con el que ya deberías estar muy familiarizado. Scope. (Y si no lo estas. ¿Pues qué estas esperando? -> Lexical Scope en Javascript)

Como estoy seguro que ya sabes el scope de una variable es el alcance que tiene la misma en el código. Javascript solo mantiene las referencias a las variables que se encuentran dentro del scope que se esta procesando. Las referencias que no cumplan con este requisito son abandonadas para que sean recolectadas por el Recolector de Basura o GC (Garbage Collector).

Pero como siempre un ejemplo dice más que mil palabras:

Referencia simple

Empecemos con una referencia a una variable dentro del global scope:

let game = {
    title: "Final Fantasy" 
}
Diagrama mostrando a el Global Scope alojando la varaible game que apunta al objeto con la propiedad title ligada al texto "Final Fantasy"

En este caso la variable game hace referencia al objeto {title: "Final Fantasy"}. El global scope es el scope principal por lo que nada de lo que este referenciado aquí será recolectado jamás por el GC.


El GC entra en acción cuando decidimos deshacernos de la referencia que existía al objeto anterior.

game = null
Diagrama mostrando a el Global Scope  en el estado anterior pero con el GC listo para recolectar el objeto.

Si a la variable game se le asigna otro valor (como null), en ese momento el objeto {title: "Final Fantasy"} se vuelve inaccessible. No hay manera de que el código recupere la referencia a él y por ende es candidato para que el GC lo recolecte y libere el espacio en memoria que ocupaba originalmente.

Referencias Múltiples

Volvamos al escenario original, pero supongamos que ahora antes de asignarle el valor de null a game, se introduce la variable en un arreglo

let game = {
    title: "Final Fantasy" 
}

let gamesList = [game]
Diagrama mostrando a el Global Scope  con 2 variables: game y gameList. game apunta  a un objeto y gameList apunta a un arreglo. El arreglo tiene un solo elemento que apunta al mismo objeto que game.

En este caso tenemos una variable llamada gameList que hace referencia a un arreglo en memoria. El arreglo solo tiene un elemento y ese elemento hace referencia al mismo objeto que game.

¿Cómo se ve el diagrama cuando game pierde la referencia al objeto?

game = null

¿Lo recolectará el Garbage Collector?

Diagrama mostrando a el Global Scope  con 2 variables: game y gameList. game no apunta  a ningun objeto y gameList apunta a un arreglo. El arreglo tiene un solo elemento que apunta  al objeto original.

Probablemente las flechitas te dieron una buena pista pero la es NO. La razón es simple. Ya que a pesar de que ya no se puede acceder a ese objeto a través de game, todavía se puede acceder a él a través del arreglo de gameList.

Referencias en local scope

En los ejemplos anteriores no nos hemos metido realmente a escenarios en los que varía el scope. Tenerlo en cuenta es ligeramente más complejo, no por el código sino porque se tienen que tener mas cosas en mente:

let game1 = {
    title: "Final Fantasy",
    sequel: game2,
    favorite: false,
    releaseDate: "1987-12-18"
}
let game2 = {
    title: "Final Fantasy X",
    prequel: game1,
    favorite: true,
    releaseDate: "2001-07-19"
}

let game3 = {
    title: "Devil May Cry 5",
    favorite: true,
    releaseDate: "2019-03-08"
}

let gamesList = [game1, game2, game3]

function getFavoriteGames(gamesList) {

    const connectionString = 'dbConnectionString'

    const favoriteGames = gamesList.filter(game => game.favorite)

    return favoriteGames
}
Diagrama mostrando a el Global Scope  con 4 variables y una función.
Las 3 primeras variables apuntan cada una a un objeto diferente y la 4° apunta a un arreglo. El arreglo tiene 3 elementos apuntando cada uno a uno de los objetos.
El objeto 1 y 2 se apuntan entre sí.

El diagrama creció bastante pero si te fijas los conceptos que tenemos son los mismos que ya viste.

Por el momento seguimos solo lidiando con el global scope, pero las referencias ahora estan presentes prácticamente entre todos los objetos.

game1, game2 y game3 hacen referencia cada uno a un objeto diferente, cada uno representando un videojuego.

El objeto de la variable game1 (el juego de Final Fantasy) hace referencia al objeto de “Final Fantasy X” a través de la propiedad de sequel.

...
let game1 = {
    title: "Final Fantasy",
    sequel: game2,
    favorite: false,
    releaseDate: "1987-12-18"
}
...

Lo mismo pasa a la inversa. “Final Fantasy X” hace referencia a “Final Fantasy” a trav[es de la propiedad de prequel.

...
let game2 = {
    title: "Final Fantasy X",
    prequel: game1,
    favorite: true,
    releaseDate: "2001-07-19"
}
...

getFavoriteGames es el nombre que se le puso a la función y a su vez hace referencia al código que se ejecutará al momento de llamarla.

gamesList es un arreglo de 3 elementos, en el que cada elemento hace referencia a uno de los videojuegos anteriores.


Las cosas cambian cuando se manda a llamar la función de getFavoriteGames.

Agrega esta línea al final del script para analizar que pasa.

let favorites = getFavoriteGames(gamesList)

Cuando se ejecuta la función las referencias en memoria se actualizan:

Diagrama igual al anterior pero agregando el getFavoriteGames local scope. Este scope contiene 3 variables. La 1° apunta al primer elemento del arreglo del global scope. La 2° a una cadena de texto. La 3° a un arreglo con 2 elementos. Dichos elementos apuntan a un objeto de los 2 existentes en el global scope.


Se crea un local scope para la función getFavoriteGames y dentro del mismo se crean nuevas referencias.

gamesList es el parámetro de entrada que recibe la función. Al pasarle el valor de la variable global de gameList, la variable local del mismo nombre copia las mismas referencias.

connectionString contiene solo una cadena de texto. En un programa real, tendría contenido mas interesante para conectarse a una base de datos real, pero de todos modos sería texto. Así que usa tu imaginación

favoriteGames es un arreglo creado por esta línea

...
const favoriteGames = gamesList.filter(game => game.favorite)
...

Lo único que hace es sacar todos los objetos referenciados en el arreglo de gameList que tienen la propiedad favorite como true. De los 3 objetos, sólo 2 cumplen con este requisito por lo que favoriteGames solo guarda la referencia a esos mismos.


¿Cuánto tiempo van a vivir en memoria todas estas referencias?

De seguro ya te lo imaginas pero en el momento en el que termina la ejecución de la función se perderán prácticamente todas.

La variable local de gamesList se elimina pero los objetos a los que hace referencia no son afectados porque las variables globales siguen apuntando a las mismas.

El valor de connectionString se vuelve inaccesible y por lot tanto ese valor si se libera de memoria.

Por último favoriteGames se elimina pero su valor se queda en memoria porque se le reasigna a la variable global favorites.

*Dicho sea de paso, si no hubieramos asignado el valor retornado de la función de getFavoriteGames entonces no se hubiera preservado nada de lo que se hizo dentro de la función.

Diagrama mostrando a el Global Scope  con 5 variables y una función.
Las 3 primeras variables apuntan cada una a un objeto diferente y la 4° apunta a un arreglo. El arreglo tiene 3 elementos apuntando cada uno a uno de los objetos.
El objeto 1 y 2 se apuntan entre sí.
La 5° variable apunta a aun arreglo cuyos 2  elementos apuntan al 2° y 3° objeto respectivamente.

Solo agregamos un ejemplo de lo que pasa al ejecutar una función, pero como puedes ver las relación entre referencias ya empieza a enmarañarse.

¿Que pasaría si borrararamos las referencias de un par de variables?

game2 = null
gameList = null

Intenta imaginartelo antes de ver la respuesta.

¿Qué pasaría con los objetos a los que hace referencia tanto game2 como gameList?

¿Listo?

Diagrama mostrando a el Global Scope  con 5 variables y una función.
Las 3 primeras variables apuntan cada una a un objeto diferente.
El objeto 1 y 2 se apuntan entre sí.
La 5° variable apunta a aun arreglo cuyos 2  elementos apuntan al 2° y 3° objeto respectivamente.
La 4° variable perdió su referencia y el GC esta listo para recolectar el espacio de memoria del arreglo al que apuntaba.

Al quitar la referencia de game2 al objeto del juego de “Final Fantasy X” no ocurre nada porque todavía hay 3 lugares que guardan referencias al mismo: la propiedad sequel del objeto de “Final Fantasy”, el arreglo de gameList y el arreglo de favorites.

Al quitar la referencia de gameList perdemos 3 referencias a 3 objetos pero todos siguen siendo accesibles desde otras propiedades. Sin embargo el espacio dispuesto para el arreglo ya no se esta usando y el GC entra en acción para recolectar esa basura.


El comportamiento de GC es una de las razones por las que se recomienda evitar mantener referencias a variables no utilizadas o a deshacerse de ellas lo mas rapido posible.

¿No es tan dificíl cierto? Como desarrolladores de JS es muy raro que tengas que lidiar con problemas de gestión de memoria pero el saber com funciona todo y el seguir estas buenas prácticas te ayudará a evitar la mayoría de problemas de optimización que puedan surgir.